﻿// This is an open source non-commercial project. Dear PVS-Studio, please check it.
// PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com

// ReSharper disable CheckNamespace
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable CommentTypo
// ReSharper disable CompareOfFloatsByEqualityOperator
// ReSharper disable IdentifierTypo
// ReSharper disable InconsistentNaming
// ReSharper disable StringLiteralTypo
// ReSharper disable UnusedMember.Local
// ReSharper disable UnusedParameter.Local

/* TextBlock.cs --
 * Ars Magna project, http://arsmagna.ru
 */

#region Using directives

using SkiaSharp;

using System;
using System.Collections.Generic;
using System.Linq;

using AM.Skia.RichTextKit.Utils;

#endregion

#nullable enable

namespace AM.Skia.RichTextKit;

/// <summary>
/// Represents a block of formatted, laid out and measurable text
/// </summary>
public class TextBlock
    : StyledText
{
    // /// <summary>
    // /// Constructor
    // /// </summary>
    // public TextBlock()
    // {
    // }

    /// <summary>
    /// The max width property sets the maximum width of a line, after which
    /// the line will be wrapped onto the next line.
    /// </summary>
    /// <remarks>
    /// This property can be set to null, in which case lines won't be wrapped.
    /// </remarks>
    public float? MaxWidth
    {
        get => _maxWidth;
        set
        {
            if (value.HasValue && value.Value < 0)
            {
                value = 0;
            }

            if (_maxWidth != value)
            {
                _maxWidth = value;
                InvalidateLayout();
            }
        }
    }

    /// <summary>
    /// This property is only used for text alignment when word wrapping
    /// is disabled (MaxWidth == null).  When set it will be used for text
    /// alignment.
    /// </summary>
    public float? RenderWidth
    {
        get => _renderWidth;
        set
        {
            if (value.HasValue && value.Value < 0)
            {
                value = 0;
            }

            if (_renderWidth != value)
            {
                _renderWidth = value;
                if (!_maxWidth.HasValue)
                {
                    InvalidateLayout();
                }
            }
        }
    }

    /// <summary>
    /// The maximum height of the TextBlock after which lines will be
    /// truncated and the final line will be appended with an
    /// ellipsis (`...`) character.
    /// </summary>
    /// <remarks>
    /// This property can be set to null, in which case the vertical height of the text block
    /// won't be capped.
    /// </remarks>
    public float? MaxHeight
    {
        get => _maxHeight;
        set
        {
            if (value.HasValue && value.Value < 0)
            {
                value = 0;
            }

            if (value != _maxHeight)
            {
                _maxHeight = value;
                InvalidateLayout();
            }
        }
    }

    /// <summary>
    /// The maximum number of lines after which lines will be
    /// truncated and the final line will be appended with an
    /// ellipsis (`...`) character.
    /// </summary>
    /// <remarks>
    /// This property can be set to null, in which case the vertical height of
    /// the text block won't be capped.
    /// </remarks>
    public int? MaxLines
    {
        get => _maxLines;
        set
        {
            if (value.HasValue && value.Value < 0)
            {
                value = 0;
            }

            if (value != _maxLines)
            {
                _maxLines = value;
                InvalidateLayout();
            }
        }
    }

    /// <summary>
    /// Controls the rendering of an ellipsis (`...`) character,
    /// when the line has been truncated because of MaxWidth/MaxHeight/MaxLines.
    /// </summary>
    /// <remarks>
    /// The default value is true, an ellipsis will be rendered.
    /// </remarks>
    public bool EllipsisEnabled
    {
        get => _ellipsisEnabled;
        set
        {
            if (value != _ellipsisEnabled)
            {
                _ellipsisEnabled = value;
                InvalidateLayout();
            }
        }
    }

    /// <summary>
    /// Sets the left, right or center alignment of the text block.
    /// </summary>
    /// <remarks>
    /// Set this property to <see cref="TextAlignment.Auto"/> to align
    /// the paragraph according to the <see cref="BaseDirection"/>.
    ///
    /// * If the <see cref="MaxWidth"/> property has been set this will
    ///   be used for alignment calculations.
    /// * If the <see cref="MaxWidth"/> property has not been set, the
    ///   width of the longest line will be used.
    /// </remarks>
    public TextAlignment Alignment
    {
        get => _textAlignment;
        set
        {
            if (_textAlignment != value)
            {
                _textAlignment = value;
                InvalidateLayout();
            }
        }
    }

    /// <summary>
    /// The base directionality of this text block (whether text is laid out
    /// left to right, or right to left)
    /// </summary>
    public TextDirection BaseDirection
    {
        get => _baseDirection;
        set
        {
            if (_baseDirection != value)
            {
                _baseDirection = value;
                InvalidateLayout();
            }
        }
    }

    /// <summary>
    /// Clear the content of this text block
    /// </summary>
    public override void Clear()
    {
        // Reset everything
        FontRun.Pool.Value!.ReturnAndClear (_fontRuns);
        TextLine.Pool.Value!.ReturnAndClear (_lines);
        _textShapingBuffers.Clear();
        base.Clear();
    }

    /// <summary>
    /// Split this text block at the specified code point index
    /// </summary>
    /// <param name="from">The code point index to copy from</param>
    /// <param name="length">The number of code points to copy</param>
    /// <returns>A new text block with the RHS split part of the text</returns>
    public TextBlock Copy (int from, int length)
    {
        // Create a new text block with the same attributes as this one
        var other = new TextBlock();
        other.Alignment = Alignment;
        other.BaseDirection = BaseDirection;
        other.MaxWidth = MaxWidth;
        other.MaxHeight = MaxHeight;
        other.MaxLines = MaxLines;

        // Copy text to the new paragraph
        foreach (var subRun in _styleRuns.GetInterectingRuns (from, length))
        {
            var sr = _styleRuns[subRun.Index];
            other.AddText (sr.CodePoints.SubSlice (subRun.Offset, subRun.Length), sr.Style);
        }

        return other;
    }

    /// <inheritdoc />
    protected override void OnChanged()
    {
        InvalidateLayout();
        base.OnChanged();
    }


    /// <summary>
    /// Appends an ellipsis to this text block
    /// </summary>
    /// <remarks>
    /// This method checks if the text block has already been truncated and if
    /// not appends an ellipsis without changing the measured vertical layout of the
    /// text block.  The ellipsis only remains in effect until the block's layout
    /// is recalculated.
    ///
    /// The text block must have at least one line.  If the block contains no text,
    /// then use AddText("\n", style) to create a single line with an attached style
    /// but no text.
    ///
    /// The intended purpose of this is to included an ellipsis on this text block
    /// when a following text block doesn't fit.
    /// </remarks>
    public void AddEllipsis()
    {
        if (!_ellipsisEnabled)
        {
            return;
        }

        // Make sure laid out
        Layout();

        // Already truncated?
        if (_truncated)
        {
            return;
        }

        if (_lines.Count == 0)
        {
            throw new InvalidOperationException ("Ellipsis can't be appended to a text block with no lines");
        }

        // Append the ellipsis
        var line = _lines[_lines.Count - 1];

        // Because adorning the line with ellipsis resets the XCoord of each font run
        // to be left aligned, we need to move the glyphs to be left aligned too
        for (var frIndex = 0; frIndex < line.Runs.Count; frIndex++)
        {
            var fr = line.Runs[frIndex];
            fr.MoveGlyphs (-fr.XCoord, 0);
        }

        // Append the ellipsis to the line and relayout theline
        AdornLineWithEllipsis (line, true);

        // Work out the new x-alignment
        var ta = ResolveTextAlignment();
        float xAdjust = 0;
        switch (ta)
        {
            case TextAlignment.Right:
                xAdjust = (_maxWidth ?? _measuredWidth) - line.Width;
                break;

            case TextAlignment.Center:
                xAdjust = ((_maxWidth ?? _measuredWidth) - line.Width) / 2;
                break;
        }

        // Adjust the measured width if the adorned line is the widest line
        if (line.Width > _measuredWidth)
        {
            _measuredWidth = line.Width;
        }

        // Reposition each run to the correct location
        for (var frIndex = 0; frIndex < line.Runs.Count; frIndex++)
        {
            var fr = line.Runs[frIndex];
            fr.Line = line;
            fr.XCoord += xAdjust;
            if (fr.RunKind == FontRunKind.Ellipsis)
            {
                // The ellipsis has it's xcoord setup, but it's glyphs have never
                // been positioned so need to handle this a little differently
                fr.MoveGlyphs (fr.XCoord, line.YCoord + line.BaseLine);
            }
            else
            {
                // Move the glyphs back to corectly aligned position
                fr.MoveGlyphs (fr.XCoord, 0);
            }
        }
    }

    /// <summary>
    /// Updates the internal layout of the text block
    /// </summary>
    /// <remarks>
    /// Generally you don't need to call this method as the layout
    /// will be automatically updated as needed.
    /// </remarks>
    public void Layout()
    {
        // Needed?
        if (!_needsLayout)
        {
            return;
        }

        _needsLayout = false;

        // Resolve max width/height
        _maxWidthResolved = _maxWidth ?? float.MaxValue;
        _maxHeightResolved = _maxHeight ?? float.MaxValue;
        _maxLinesResolved = _maxLines ?? int.MaxValue;

        // Reset layout state
        _textShapingBuffers.Clear();
        _fontRuns.Clear();
        _lines.Clear();
        _caretIndicies.Clear();
        _wordBoundaryIndicies.Clear();
        _measuredHeight = 0;
        _measuredWidth = 0;
        _leftOverhang = null;
        _rightOverhang = null;
        _truncated = false;

        // Only layout if actually have some text
        if (_codePoints.Length != 0)
        {
            // Build font runs
            BuildFontRuns();

            // Break font runs into lines
            BreakLines();

            // Finalize lines
            FinalizeLines();
        }
    }

    /// <summary>
    /// Get all font runs for this text block
    /// </summary>
    public IReadOnlyList<FontRun> FontRuns
    {
        get
        {
            Layout();
            return _fontRuns;
        }
    }

    /// <summary>
    /// Get all the lines for this text block
    /// </summary>
    public IReadOnlyList<TextLine> Lines
    {
        get
        {
            Layout();
            return _lines;
        }
    }

    /// <summary>
    /// Paint this text block
    /// </summary>
    /// <param name="canvas">The Skia canvas to paint to</param>
    /// <param name="options">Options controlling the paint operation</param>
    public void Paint (SKCanvas canvas, TextPaintOptions? options = null)
    {
        // Ensure have options
        options ??=TextPaintOptions.Default;

        // Ensure layout done
        Layout();

        // Create context
        var ctx = new PaintTextContext()
        {
            Canvas = canvas,
            Options = options,
        };

        // Prepare selection
        if (options.Selection.HasValue)
        {
            ctx.SelectionStart = options.Selection.Value.Minimum;
            ctx.SelectionEnd = options.Selection.Value.Maximum;
            ctx.PaintSelectionBackground = new SKPaint()
            {
                Color = options.SelectionColor,
                IsStroke = false,
                IsAntialias = false,
            };
            if (options.SelectionHandleScale != 0 && options.SelectionHandleColor.Alpha > 0)
            {
                ctx.SelectionHandleScale = options.SelectionHandleScale;
                ctx.PaintSelectionHandle = new SKPaint()
                {
                    Color = options.SelectionHandleColor,
                    IsStroke = false,
                    IsAntialias = true,
                };
            }
        }
        else
        {
            ctx.SelectionStart = -1;
            ctx.SelectionEnd = -1;
        }

        // Paint each line
        foreach (var l in _lines)
        {
            l.Paint (ctx);
        }

        // Clean up
        ctx.PaintSelectionBackground!.Dispose();
    }

    /// <summary>
    /// Paint this text block
    /// </summary>
    /// <param name="canvas">The Skia canvas to paint to</param>
    /// <param name="position">The top left position within the canvas to draw at</param>
    /// <param name="options">Options controlling the paint operation</param>
    public void Paint (SKCanvas canvas, SKPoint position, TextPaintOptions? options = null)
    {
        // Translate
        canvas.Save();
        canvas.Translate (position.X, position.Y);

        // Paint it
        Paint (canvas, options);

        // Restore and done!
        canvas.Restore();
    }

    /// <summary>
    /// The total height of all lines.
    /// </summary>
    public float MeasuredHeight
    {
        get
        {
            Layout();
            return _measuredHeight;
        }
    }

    /// <summary>
    /// The length of the displayed text (in code points)
    /// </summary>
    /// <remarks>
    /// If the text is truncated, this is the index of the point
    /// at which the ellipsis was inserted.  If the text it not
    /// truncated, is the length of all added text.
    /// </remarks>
    public int MeasuredLength
    {
        get
        {
            Layout();
            if (_lines.Count == 0)
            {
                return 0;
            }

            return _lines[_lines.Count - 1].End;
        }
    }


    /// <summary>
    /// The number of lines in the text
    /// </summary>
    public int LineCount
    {
        get
        {
            Layout();
            return _lines.Count;
        }
    }

    /// <summary>
    /// The width of the widest line of text.
    /// </summary>
    /// <remarks>
    /// The returned width does not include any overhang.
    /// </remarks>
    public float MeasuredWidth
    {
        get
        {
            Layout();
            return _measuredWidth;
        }
    }

    /// <summary>
    /// Indicates if the text was truncated due to max height or max lines
    /// constraints
    /// </summary>
    public bool Truncated
    {
        get
        {
            Layout();
            return _truncated;
        }
    }


    /// <summary>
    /// Gets the size of any unused space around the text.
    /// </summary>
    /// <remarks>
    /// If MaxWidth is not set, the left and right padding will always be zero.
    ///
    /// This property also returns a bottom padding amount if MaxHeight is set.
    ///
    /// The returned top padding is always zero.
    ///
    /// The return rectangle describes padding amounts for each edge - not
    /// rectangle co-ordinates.
    /// </remarks>
    public SKRect MeasuredPadding
    {
        get
        {
            var r = new SKRect();

            // Bottom padding?
            if (_maxHeight.HasValue)
            {
                r.Bottom = _maxHeight.Value - _measuredHeight;
            }

            if (!_maxWidth.HasValue)
            {
                return r;
            }

            Layout();

            switch (ResolveTextAlignment())
            {
                case TextAlignment.Left:
                    r.Left = 0;
                    r.Right = _maxWidthResolved - _measuredWidth;
                    return r;

                case TextAlignment.Right:
                    r.Left = _maxWidthResolved - _measuredWidth;
                    r.Right = 0;
                    return r;

                case TextAlignment.Center:
                    r.Left = (_maxWidthResolved - _measuredWidth) / 2;
                    r.Right = (_maxWidthResolved - _measuredWidth) / 2;
                    return r;
            }

            throw new InvalidOperationException();
        }
    }


    /// <summary>
    /// Gets the actual measured overhang in each direction based on the
    /// fonts used, and the supplied text.
    /// </summary>
    /// <remarks>
    /// The return rectangle describes overhang amounts for each edge - not
    /// rectangle co-ordinates.
    /// </remarks>
    public SKRect MeasuredOverhang
    {
        get
        {
            Layout();
            if (!_leftOverhang.HasValue)
            {
                var right = _maxWidth ?? MeasuredWidth;
                float leftOverhang = 0;
                float rightOverhang = 0;
                foreach (var l in _lines)
                {
                    l.UpdateOverhang (right, ref leftOverhang, ref rightOverhang);
                }

                _leftOverhang = leftOverhang;
                _rightOverhang = rightOverhang;
            }

            return new SKRect (_leftOverhang.Value, 0, _rightOverhang!.Value, 0);
        }
    }

    /// <summary>
    /// Hit test this block of text
    /// </summary>
    /// <param name="lineIndex">The line to be hit test</param>
    /// <param name="x">The x-coordinate relative to top left of the block</param>
    /// <returns>A HitTestResult</returns>
    public HitTestResult HitTestLine (int lineIndex, float x)
    {
        return _lines[lineIndex].HitTest (x);
    }

    /// <summary>
    /// Hit test this block of text
    /// </summary>
    /// <param name="x">The x-coordinate relative to top left of the block</param>
    /// <param name="y">The x-coordinate relative to top left of the block</param>
    /// <returns>A HitTestResult</returns>
    public HitTestResult HitTest (float x, float y)
    {
        Layout();

        var htr = new HitTestResult();

        // Work out which line number we're over
        htr.OverLine = -1;
        htr.OverCodePointIndex = -1;
        for (var i = 0; i < _lines.Count; i++)
        {
            var l = _lines[i];
            if (y >= l.YCoord && y < l.YCoord + l.Height)
            {
                htr.OverLine = i;
            }
        }

        // Work out the closest line
        if (htr.OverLine >= 0)
        {
            htr.ClosestLine = htr.OverLine;
        }
        else if (y < 0)
        {
            htr.ClosestLine = 0;
        }
        else
        {
            htr.ClosestLine = _lines.Count - 1;
        }

        // Hit test each cluster
        if (htr.ClosestLine >= 0 && htr.ClosestLine < _lines.Count)
        {
            // Hit test the line
            var l = _lines[htr.ClosestLine];
            l.HitTest (x, ref htr);
        }

        // If we're not over the line, we're also not over the character
        if (htr.OverLine < 0)
        {
            htr.OverCodePointIndex = -1;
        }

        if (htr.ClosestCodePointIndex < 0)
        {
            if (htr.ClosestLine == _lines.Count - 1 && _lines.Count > 0)
            {
                htr.ClosestCodePointIndex = _lines[_lines.Count - 1].End - 1;
            }
            else
            {
                htr.ClosestCodePointIndex = 0;
            }
        }

        return htr;
    }


    /// <summary>
    /// Build map of all caret positions
    /// </summary>
    private void BuildCaretIndicies()
    {
        Layout();
        if (_caretIndicies.Count == 0)
        {
            foreach (var r in _lines.SelectMany (x => x.Runs))
            {
                for (var i = 0; i < r.Clusters.Length; i++)
                {
                    _caretIndicies.Add (r.Clusters[i]);
                }
            }

            _caretIndicies.Add (MeasuredLength);
            _caretIndicies = _caretIndicies.OrderBy (x => x).Distinct().ToList();
        }
    }

    /// <summary>
    /// Retrieves a list of all valid caret positions
    /// </summary>
    public IReadOnlyList<int> CaretIndicies
    {
        get
        {
            BuildCaretIndicies();
            return _caretIndicies;
        }
    }

    /// <summary>
    /// Retrieves a list of all valid caret positions
    /// </summary>
    public IReadOnlyList<int> WordBoundaryIndicies
    {
        get
        {
            // Find word boundaries (if not already done)
            if (_wordBoundaryIndicies.Count == 0)
            {
                _wordBoundaryIndicies = WordBoundaryAlgorithm.FindWordBoundaries (_codePoints.AsSlice()).ToList();
            }

            return _wordBoundaryIndicies;
        }
    }


    /// <summary>
    /// Retrieves a list of the indicies of the first code point in each line
    /// </summary>
    public IReadOnlyList<int> LineIndicies
    {
        get { return _lines.Select (x => x.Start).ToList(); }
    }


    /// <summary>
    /// Given a code point index, find the index in the CaretIndicies
    /// </summary>
    /// <param name="codePointIndex">The code point index to lookup</param>
    /// <returns>The index in the code point idnex in the CaretIndicies array</returns>
    public int LookupCaretIndex (int codePointIndex)
    {
        BuildCaretIndicies();
        var index = _caretIndicies.BinarySearch (codePointIndex);
        if (index < 0)
        {
            index = ~index;
        }

        return index;
    }

    /// <summary>
    /// Calculates useful information for displaying a caret
    /// </summary>
    /// <remarks>
    /// When altPosition is true, if the code point index indicates the first
    /// code point after a line break, the returned caret position will be the
    /// end of the previous line (instead of the start of the next line)
    /// </remarks>
    /// <param name="position">The caret position</param>
    /// <returns>A CaretInfo struct</returns>
    public CaretInfo GetCaretInfo (CaretPosition position)
    {
        // Empty text block?
        if (_codePoints.Length == 0 || position.CodePointIndex < 0 || _lines.Count == 0)
        {
            return CaretInfo.None;
        }

        // Past the measured length?
        if (position.CodePointIndex > MeasuredLength)
        {
            return CaretInfo.None;
        }

        // Look up the caret index
        var cpii = LookupCaretIndex (position.CodePointIndex);

        // Create caret info
        var ci = new CaretInfo();
        ci.CodePointIndex = _caretIndicies[cpii];

        var frIndex = FindFontRunForCodePointIndex (position.CodePointIndex);
        FontRun? fontRun = null;
        if (frIndex >= 0)
        {
            fontRun = _fontRuns[frIndex];

            if (fontRun.Start == position.CodePointIndex && frIndex > 0)
            {
                var frPrior = _fontRuns[frIndex - 1];
                if (frPrior.End == position.CodePointIndex)
                {
                    if (position.AltPosition ||
                        (frPrior.Direction == TextDirection.RTL &&
                         frPrior.RunKind != FontRunKind.TrailingWhitespace))
                    {
                        fontRun = frPrior;
                    }
                }
            }
        }
        else
        {
            var lastLine = _lines[_lines.Count - 1];
            if (lastLine.RunsInternal.Count > 0)
            {
                fontRun = lastLine.RunsInternal[lastLine.RunsInternal.Count - 1];
            }
        }

        if (fontRun == null)
        {
            return CaretInfo.None;
        }

        // Setup caret coordinates
        ci.CaretXCoord = ci.CodePointIndex < 0 ? 0 : fontRun.GetXCoordOfCodePointIndex (ci.CodePointIndex);
        ci.CaretRectangle = CalculateCaretRectangle (ci, fontRun);
        ci.LineIndex = _lines.IndexOf (fontRun.Line!);

        return ci;
    }

    private SKRect CalculateCaretRectangle (CaretInfo ci, FontRun fr)
    {
        if (ci.CodePointIndex < 0)
        {
            return SKRect.Empty;
        }

        // Get the font run to be used for caret metrics
        fr = GetFontRunForCaretMetrics (ci, fr);

        // Setup the basic rectangle
        var rect = new SKRect();
        rect.Left = ci.CaretXCoord;
        rect.Top = fr.Line!.YCoord + fr.Line.BaseLine + fr.Ascent;
        rect.Right = rect.Left;
        rect.Bottom = fr.Line.YCoord + fr.Line.BaseLine + fr.Descent;

        // Apply slant if italic
        if (fr.Style!.FontItalic)
        {
            rect.Left -= rect.Height / 14;
            rect.Right = rect.Left + rect.Height / 5;
        }

        return rect;
    }

    /// <summary>
    /// Internal helper to get the font run that should
    /// be used for caret metrics.
    /// </summary>
    /// <remarks>
    /// The returned font run is the font run of the previous
    /// character, or the same character if the first font run
    /// on the line.
    /// </remarks>
    /// <returns>The determined font run</returns>
    private FontRun GetFontRunForCaretMetrics (CaretInfo ci, FontRun fr)
    {
        // Same font run?
        if (ci.CodePointIndex > fr.Start)
        {
            return fr;
        }

        // Try to get the previous font run in this line
        var lineRuns = fr.Line!.Runs as List<FontRun>;
        var index = lineRuns!.IndexOf (fr);
        if (index <= 0)
        {
            return fr;
        }

        // Use the previous font run
        return lineRuns[index - 1];
    }


    /// <summary>
    /// Find the font run holding a code point index
    /// </summary>
    /// <param name="codePointIndex"></param>
    /// <returns></returns>
    public int FindFontRunForCodePointIndex (int codePointIndex)
    {
        // Past end of text?
        if (codePointIndex > MeasuredLength)
        {
            return -1;
        }

        // Look up font run
        var frIndex =
            FontRuns.BinarySearch (codePointIndex, (run, value) => { return (run.End - 1) - codePointIndex; });
        if (frIndex < 0)
        {
            frIndex = ~frIndex;
        }

        if (frIndex == _fontRuns.Count)
        {
            frIndex = _fontRuns.Count - 1;
        }

        if (frIndex < 0)
        {
            return -1;
        }

        // Return the font run
        var fr = _fontRuns[frIndex];
        System.Diagnostics.Debug.Assert (codePointIndex >= fr.Start);
        System.Diagnostics.Debug.Assert (codePointIndex <= fr.End);
        return frIndex;
    }

    /// <summary>
    /// Invalidate the layout
    /// </summary>
    private void InvalidateLayout()
    {
        // Make sure style runs are valid (debug only)
        _styleRuns.CheckValid (_codePoints.Length);

        // Set layout flag
        _needsLayout = true;
    }

    /// <summary>
    /// Set if the current layout is dirty
    /// </summary>
    private bool _needsLayout = true;

    /// <summary>
    /// Maximum width (wrap point, or null for no wrapping)
    /// </summary>
    private float? _maxWidth;

    /// <summary>
    /// Render width (used for alignment with maxwidth is null)
    /// </summary>
    private float? _renderWidth;

    /// <summary>
    /// Width at which to wrap content
    /// </summary>
    private float _maxWidthResolved = float.MaxValue;

    /// <summary>
    /// Maximum height (crop lines after this)
    /// </summary>
    private float? _maxHeight;

    /// <summary>
    /// Maximum layout height
    /// </summary>
    private float _maxHeightResolved = float.MaxValue;

    /// <summary>
    /// Maximum number of lines
    /// </summary>
    private int? _maxLines;

    /// <summary>
    /// Maximum number of lines
    /// </summary>
    private int _maxLinesResolved = int.MaxValue;

    /// <summary>
    /// Option to control ellipsis
    /// </summary>
    private bool _ellipsisEnabled = true;

    /// <summary>
    /// Text alignment
    /// </summary>
    private TextAlignment _textAlignment = TextAlignment.Auto;

    /// <summary>
    /// Base direction as set by user
    /// </summary>
    private TextDirection _baseDirection = TextDirection.Auto;

    /// <summary>
    /// Base direction as resolved if auto
    /// </summary>
    private TextDirection _resolvedBaseDirection;

    /// <summary>
    /// Re-usable buffers for text shaping results
    /// </summary>
    private TextShaper.ResultBufferSet _textShapingBuffers = new ();

    /// <summary>
    /// Reusable buffer for bidi data
    /// </summary>
    private BidiData _bidiData = new ();

    /// <summary>
    /// A list of font runs, after splitting by directionality, user styles and font fallback
    /// </summary>
    private List<FontRun> _fontRuns = new ();

    /// <summary>
    /// Helper for splitting code into linebreaks
    /// </summary>
    private LineBreaker _lineBreaker = new ();

    /// <summary>
    /// The measured height
    /// </summary>
    private float _measuredHeight;

    /// <summary>
    /// The measured width
    /// </summary>
    private float _measuredWidth;

    /// <summary>
    /// The required left overhang
    /// </summary>
    private float? _leftOverhang;

    /// <summary>
    /// The required left overhang
    /// </summary>
    private float? _rightOverhang;

    /// <summary>
    /// Indicates if the text was truncated by max height/max lines limitations
    /// </summary>
    private bool _truncated;

    /// <summary>
    /// The final laid out set of lines
    /// </summary>
    private List<TextLine> _lines = new ();

    /// <summary>
    /// Calculated valid caret indicies
    /// </summary>
    private List<int> _caretIndicies = new ();

    /// <summary>
    /// Calculated word boundary caret indicies
    /// </summary>
    private List<int> _wordBoundaryIndicies = new ();

    /// <summary>
    /// Resolve the text alignment when set to Auto
    /// </summary>
    /// <returns>Resolved text alignment (left, right or center)</returns>
    internal TextAlignment ResolveTextAlignment()
    {
        if (_textAlignment == TextAlignment.Auto)
        {
            return _resolvedBaseDirection == TextDirection.LTR ? TextAlignment.Left : TextAlignment.Right;
        }
        else
        {
            return _textAlignment;
        }
    }


    /// <summary>
    /// Split into runs based on directionality and style switch points
    /// </summary>
    private void BuildFontRuns()
    {
        // Use the shared Bidi algo instance
        var bidi = Bidi.Instance.Value;

        var originalLength = _codePoints.Length;
        try
        {
            // Clearn unshaped run buffer
            _unshapedRuns.Clear();

            // Break supplied text into directionality runs
            _bidiData.Init (_codePoints.AsSlice(), (sbyte)_baseDirection);

            // If we have embedded directional overrides then change those
            // ranges to neutral
            if (_hasTextDirectionOverrides)
            {
                // Save types
                _bidiData.SaveTypes();

                for (var i = 0; i < _styleRuns.Count; i++)
                {
                    // Get the run
                    var sr = _styleRuns[i];

                    // Does it have a direction override?
                    if (sr.Style!.TextDirection == TextDirection.Auto)
                    {
                        continue;
                    }

                    // Change the range to neutral with no brackets
                    _bidiData.Types.SubSlice (sr.Start, sr.Length).Fill (Directionality.ON);
                    _bidiData.PairedBracketTypes.SubSlice (sr.Start, sr.Length).Fill (PairedBracketType.n);
                }
            }

            // Process bidi
            bidi!.Process (_bidiData);

            var resolvedLevels = bidi.ResolvedLevels;

            // Get resolved direction
            _resolvedBaseDirection = (TextDirection)bidi.ResolvedParagraphEmbeddingLevel;

            // Now process the embedded runs
            if (_hasTextDirectionOverrides)
            {
                // Restore types
                _bidiData.RestoreTypes();

                // Process each run individually
                for (var i = 0; i < _styleRuns.Count; i++)
                {
                    // Get the run
                    var sr = _styleRuns[i];

                    // Does it have a direction override?
                    if (sr.Style!.TextDirection == TextDirection.Auto)
                    {
                        continue;
                    }

                    // Get the style run bidi data
                    var types = _bidiData.Types.SubSlice (sr.Start, sr.Length);
                    var pbts = _bidiData.PairedBracketTypes.SubSlice (sr.Start, sr.Length);
                    var pbvs = _bidiData.PairedBracketValues.SubSlice (sr.Start, sr.Length);

                    // Get a temp buffer to store the results
                    // (We can't use the Bidi's built in buffer because we're about to patch it)
                    var levels = _bidiData.GetTempLevelBuffer (sr.Length);

                    // Process this style run
                    bidi.Process (types, pbts, pbvs, (sbyte)sr.Style.TextDirection, _bidiData.HasBrackets,
                        _bidiData.HasEmbeddings, _bidiData.HasIsolates, levels);

                    // Copy result levels back to the full level set
                    resolvedLevels.SubSlice (sr.Start, sr.Length).Set (levels);
                }
            }

            // Get the list of directional runs
            var bidiRuns = BidiRun.CoalescLevels (resolvedLevels).ToList();

            // Split...
            var pos = 0;
            var bidiRun = 0;
            var styleRun = 0;
            while (pos < _codePoints.Length)
            {
                // Move to next bidi/style run
                if (pos == bidiRuns[bidiRun].End)
                {
                    bidiRun++;
                }

                if (pos == _styleRuns[styleRun].End)
                {
                    styleRun++;
                }

                // Work out where this run ends
                var nextPos = Math.Min (bidiRuns[bidiRun].End, _styleRuns[styleRun].End);

                // Add the run
                var dir = bidiRuns[bidiRun].Direction == Directionality.L ? TextDirection.LTR : TextDirection.RTL;
                AddDirectionalRun (_styleRuns[styleRun], pos, nextPos - pos, dir, _styleRuns[styleRun].Style!);

                // Move to next position
                pos = nextPos;
            }

            System.Diagnostics.Debug.Assert (bidiRun == bidiRuns.Count - 1);
            System.Diagnostics.Debug.Assert (styleRun == _styleRuns.Count - 1);

            // Add the final run
            var dir2 = bidiRuns[bidiRun].Direction == Directionality.L ? TextDirection.LTR : TextDirection.RTL;
            AddDirectionalRun
                (
                    _styleRuns[_styleRuns.Count - 1],
                    pos,
                    _codePoints.Length - pos,
                    dir2,
                    _styleRuns[styleRun].Style!
                );

            // Flush runs
            FlushUnshapedRuns();
        }
        catch (Exception x)
        {
            throw new InvalidOperationException (
                $"Exception in BuildFontRuns() with original length of {originalLength} now {_codePoints.Length}, style run count {_styleRuns.Count}, font run count {_fontRuns.Count}, direction overrides: {_hasTextDirectionOverrides}",
                x);
        }
    }

    /// <summary>
    /// Gets the Skia type face for a IStyle
    /// </summary>
    /// <param name="style">The style</param>
    /// <param name="ignoreFontVariants">When true, doesn't embolden super/sub scripts</param>
    /// <returns>The Skia typeface</returns>
    private SKTypeface TypefaceFromStyle (IStyle style, bool ignoreFontVariants = false)
    {
        return (FontMapper ?? FontMapper.Default).TypefaceFromStyle (style, ignoreFontVariants);
    }


    /// <summary>
    /// Gets or sets the font mapper to be used by this TextBlock instance
    /// </summary>
    /// <remarks>
    /// When null, the default font mapper (FontMapper.Default) is used.
    /// </remarks>
    public FontMapper? FontMapper { get; set; }

    /// <summary>
    /// Adds a run of directional text
    /// </summary>
    /// <param name="styleRun">The style run the directional run was created from</param>
    /// <param name="start">Index of the first code point _codePoints buffer</param>
    /// <param name="length">Number of code points in this run</param>
    /// <param name="direction">The direction of the text</param>
    /// <param name="style">The user supplied style for this run</param>
    private void AddDirectionalRun (StyleRun styleRun, int start, int length, TextDirection direction, IStyle style)
    {
        // Quit if redundant...
        if (length == 0)
        {
            return;
        }

        // Get the typeface
        var typeface = TypefaceFromStyle (style);

        // Get the slice of code points
        var codePointsSlice = _codePoints.SubSlice (start, length);

        // Split into font fallback runs
        foreach (var fontRun in FontFallback.GetFontRuns (codePointsSlice, typeface, style.ReplacementCharacter))
        {
            // Add this run
            AddFontRun (styleRun, start + fontRun.Start, fontRun.Length, direction, style, fontRun.Typeface, typeface);
        }
    }

    /// <summary>
    /// Adds a run of single font text
    /// </summary>
    /// <param name="styleRun">The style run the directional run was created from</param>
    /// <param name="start">Index of the first code point _codePoints buffer</param>
    /// <param name="length">Number of code points in this run</param>
    /// <param name="direction">The direction of the text</param>
    /// <param name="style">The user supplied style for this run</param>
    /// <param name="typeface">The typeface of the run</param>
    /// <param name="asFallbackFor">The typeface this is a fallback for</param>
    private void AddFontRun (StyleRun styleRun, int start, int length, TextDirection direction, IStyle style,
        SKTypeface typeface, SKTypeface asFallbackFor)
    {
        // Coalesc added font runs such that those with the exact same font, direction etc... are
        // all shaped together such that kerning is correct for the entire run - regardless of
        // non-typographic style changes (eg: underline, color etc...).
        var usr = new UnshapedRun()
        {
            styleRun = styleRun,
            start = start,
            length = length,
            direction = direction,
            style = style,
            typeface = typeface,
            asFallbackFor = asFallbackFor,
        };

        // If this run can't be shaped with the previous one then flush those accumlated so far
        if (_unshapedRuns.Count > 0 && !_unshapedRuns[_unshapedRuns.Count - 1].CanShapeWith (usr))
        {
            FlushUnshapedRuns();
        }

        // Store it for now, actually shape it once we determine the next run is really
        // a different font
        _unshapedRuns.Add (usr);
    }

    private void FlushUnshapedRuns()
    {
        // Quit if nothing to do?
        if (_unshapedRuns.Count == 0)
        {
            return;
        }

        if (_unshapedRuns.Count == 1)
        {
            // If there's only one accumulated shape run then we can shape and add it directly
            var usr = _unshapedRuns[0];
            _fontRuns.Add (CreateFontRun (usr.styleRun!, _codePoints.SubSlice (usr.start, usr.length), usr.direction,
                usr.style!, usr.typeface!, usr.asFallbackFor!));
        }
        else
        {
            // There are multiple styled runs, but they're all using the exact same type face
            // so shape them all together to maintain correct kerning, but then break them
            // apart so they're rendered separately with correct style for each piece.
            var first = _unshapedRuns[0];
            var last = _unshapedRuns[_unshapedRuns.Count - 1];
            var spanningRun = CreateFontRun (first.styleRun!,
                _codePoints.SubSlice (first.start, last.start + last.length - first.start), first.direction,
                first.style!, first.typeface!, first.asFallbackFor!);

            for (var i = 1; i < _unshapedRuns.Count; i++)
            {
                var newRun = spanningRun.Split (_unshapedRuns[i].start);

                spanningRun.StyleRun = _unshapedRuns[i - 1].styleRun!;
                spanningRun.Style = _unshapedRuns[i - 1].style!;

                _fontRuns.Add (spanningRun);
                spanningRun = newRun;
            }

            spanningRun.StyleRun = _unshapedRuns[_unshapedRuns.Count - 1].styleRun!;
            spanningRun.Style = _unshapedRuns[_unshapedRuns.Count - 1].style!;
            _fontRuns.Add (spanningRun);
        }

        _unshapedRuns.Clear();
    }

    private List<UnshapedRun> _unshapedRuns = new ();

    private class UnshapedRun
    {
        public StyleRun? styleRun;
        public int start;
        public int length;
        public TextDirection direction;
        public IStyle? style;
        public SKTypeface? typeface;
        public SKTypeface? asFallbackFor;

        // Check if this unshaped run can be shaped with the next one
        public bool CanShapeWith (UnshapedRun next)
        {
            return typeface == next.typeface &&
                   style!.FontSize == next.style!.FontSize &&
                   asFallbackFor == next.asFallbackFor &&
                   direction == next.direction &&
                   start + length == next.start;
        }
    }


    /// <summary>
    /// Helper to create a font run
    /// </summary>
    /// <param name="styleRun">The style run owning this font run</param>
    /// <param name="codePoints">The code points of the run</param>
    /// <param name="direction">The run direction</param>
    /// <param name="style">The user supplied style for this run</param>
    /// <param name="typeface">The typeface of the run</param>
    /// <param name="asFallbackFor">The original typeface this is a fallback for</param>
    /// <returns>A FontRun</returns>
    private FontRun CreateFontRun (StyleRun styleRun, Slice<int> codePoints, TextDirection direction, IStyle style,
        SKTypeface typeface, SKTypeface asFallbackFor)
    {
        // Shape the text
        var shaper = TextShaper.ForTypeface (typeface);
        TextShaper.Result shaped;
        if (style.ReplacementCharacter == '\0')
        {
            shaped = shaper.Shape (_textShapingBuffers, codePoints, style, direction, codePoints.Start, asFallbackFor,
                ResolveTextAlignment());
        }
        else
        {
            shaped = shaper.ShapeReplacement (_textShapingBuffers, codePoints, style, codePoints.Start);
        }

        // Create the run
        var fontRun = FontRun.Pool.Value!.Get();
        fontRun.StyleRun = styleRun;
        fontRun.CodePointBuffer = _codePoints;
        fontRun.Start = codePoints.Start;
        fontRun.Length = codePoints.Length;
        fontRun.Style = style;
        fontRun.Direction = direction;
        fontRun.Typeface = typeface;
        fontRun.Glyphs = shaped.GlyphIndicies;
        fontRun.GlyphPositions = shaped.GlyphPositions;
        fontRun.RelativeCodePointXCoords = shaped.CodePointXCoords;
        fontRun.Clusters = shaped.Clusters;
        fontRun.Ascent = shaped.Ascent;
        fontRun.Descent = shaped.Descent;
        fontRun.Leading = shaped.Leading;
        fontRun.Width = shaped.EndXCoord.X;
        return fontRun;
    }

    /// <summary>
    /// Break the list of font runs into lines
    /// </summary>
    private void BreakLines()
    {
        // Work out possible line break positions
        _lineBreaker.Reset (_codePoints.AsSlice());
        var lineBreakPositions = _lineBreaker.GetBreaks (!_maxWidth.HasValue);

        var frIndexStartOfLine = 0; // Index of the first font run in the current line
        var frIndex = 0; // Index of the current font run
        var lbrIndex = 0; // Index of the next unconsumed linebreak
        float consumedWidth = 0; // Total width of all font runs placed on this line
        var frSplitIndex = -1; // Index of the last font run that contained fitting break point
        var codePointIndexSplit = -1; // Code point index of the last fitting break point measure position
        var codePointIndexWrap = -1; // Code point index of the last fitting breaj point wrap position

        if (!CheckHeightConstraints())
        {
            return;
        }

        while (frIndex < _fontRuns.Count)
        {
            // Get the font run, update it's position
            // and move to next
            var fr = _fontRuns[frIndex];
            fr.XCoord = consumedWidth;
            consumedWidth += fr.Width;

            // Skip line breaks
            var breakLine = false;
            while (lbrIndex < lineBreakPositions.Count)
            {
                // Past this run?
                var lbr = lineBreakPositions[lbrIndex];
                if (lbr.PositionMeasure < fr.Start)
                {
                    lbrIndex++;
                    continue;
                }

                if (lbr.PositionMeasure >= fr.End)
                {
                    break;
                }

                // Do we need to break
                var totalWidthToThisBreakPoint = fr.XCoord + fr.LeadingWidth (lbr.PositionMeasure);
                if (totalWidthToThisBreakPoint > _maxWidthResolved)
                {
                    breakLine = true;
                    break;
                }

                // It fits, remember that
                lbrIndex++;

                // Only mark that we have something that fits if we actually know something has (or will be) been consumed
                if (totalWidthToThisBreakPoint > 0 || lbr.PositionWrap > lbr.PositionMeasure)
                {
                    frSplitIndex = frIndex;
                    codePointIndexSplit = lbr.PositionMeasure;
                    codePointIndexWrap = lbr.PositionWrap;
                }

                if (lbr.Required)
                {
                    breakLine = true;
                    break;
                }
            }

            // If we're on the last run and we've exceeded the width limit then force a break
            if (!breakLine && frIndex + 1 == _fontRuns.Count && consumedWidth > _maxWidthResolved)
            {
                breakLine = true;
            }

            // Break the line here?
            if (!breakLine)
            {
                frIndex++;
                continue;
            }

            // If there wasn't a line break anywhere in the line, then we need to force one
            // on a character boundary.  Also do this if we know we're on the last available line.
            if (frSplitIndex < 0 || (_maxLines.HasValue && _lines.Count == _maxLines.Value - 1))
            {
                // Get the last run that partially fitted
                while (frIndex > frIndexStartOfLine && _fontRuns[frIndex].XCoord > _maxWidthResolved)
                {
                    frIndex--;
                }

//                    frIndex = frIndexStartOfLine;
                fr = _fontRuns[frIndex];
                var room = _maxWidthResolved - fr.XCoord;
                frSplitIndex = frIndex;
                codePointIndexSplit = fr.FindBreakPosition (room, frSplitIndex == frIndexStartOfLine);
                codePointIndexWrap = codePointIndexSplit;
                while (codePointIndexWrap < _codePoints.Length &&
                       UnicodeClasses.LineBreakClass (_codePoints[codePointIndexWrap]) == LineBreakClass.SP)
                    codePointIndexWrap++;
            }

            // Split it
            fr = _fontRuns[frSplitIndex];
            if (codePointIndexSplit == fr.Start)
            {
                // Split exactly before the run
            }
            else if (codePointIndexSplit == fr.End)
            {
                // Split exactly after the run
                frSplitIndex++;
            }
            else
            {
                // Split in the middle of the run
                frSplitIndex++;
                _fontRuns.Insert (frSplitIndex, fr.Split (codePointIndexSplit));
            }

            // Trailing whitespace
            var frTrailingWhiteSpaceIndex = frSplitIndex;
            while (frTrailingWhiteSpaceIndex < _fontRuns.Count &&
                   _fontRuns[frTrailingWhiteSpaceIndex].Start < codePointIndexWrap)
            {
                if (codePointIndexWrap < _fontRuns[frTrailingWhiteSpaceIndex].End)
                {
                    var fr2 = _fontRuns[frTrailingWhiteSpaceIndex].Split (codePointIndexWrap);
                    _fontRuns.Insert (frTrailingWhiteSpaceIndex + 1, fr2);
                }

                frTrailingWhiteSpaceIndex++;
            }

            // Build the final line
            BuildLine (frIndexStartOfLine, frSplitIndex, frTrailingWhiteSpaceIndex);

            // Reset for the next line
            frSplitIndex = -1;
            frIndex = frTrailingWhiteSpaceIndex;
            frIndexStartOfLine = frIndex;
            consumedWidth = 0;

            // Check height constraints and quit if finished
            if (!CheckHeightConstraints())
            {
                return;
            }
        }

        // Build the final line
        if (frIndexStartOfLine < _fontRuns.Count)
        {
            BuildLine (frIndexStartOfLine, _fontRuns.Count, _fontRuns.Count);
            CheckHeightConstraints();
        }
    }

    /// <summary>
    /// Construct a single line from the specified font run indicies
    /// </summary>
    /// <param name="frIndexStartOfLine">Index of the first font run in the line</param>
    /// <param name="frSplitIndex">Index of the last type</param>
    /// <param name="frTrailingWhiteSpaceIndex"></param>
    private void BuildLine (int frIndexStartOfLine, int frSplitIndex, int frTrailingWhiteSpaceIndex)
    {
        // Create the line

        var line = TextLine.Pool.Value!.Get();
        line.TextBlock = this;
        line.YCoord = _measuredHeight;

        // Add runs
        for (var i = frIndexStartOfLine; i < frTrailingWhiteSpaceIndex; i++)
        {
            // Tag trailing whitespace appropriately
            if (i >= frSplitIndex)
            {
                var fr = _fontRuns[i];
                fr.RunKind = FontRunKind.TrailingWhitespace;
                if (fr.Direction != _resolvedBaseDirection)
                {
                    // What is this a fallback for?
                    var asFallbackFor = TypefaceFromStyle (fr.Style!, ignoreFontVariants: false);

                    // Create a new font run over the same text span but using the base direction
                    _fontRuns[i] = CreateFontRun (fr.StyleRun!, fr.CodePoints, _resolvedBaseDirection, fr.Style!,
                        fr.Typeface!, asFallbackFor);
                }
            }

            line.RunsInternal.Add (_fontRuns[i]);
        }

        // Add the line to the collection
        _lines.Add (line);

        // Lay it out
        LayoutLine (line);

        // Update y position
        _measuredHeight += line.Height;
    }

    /// <summary>
    /// Layout the font runs within a line
    /// </summary>
    /// <param name="line">The line to layout</param>
    private void LayoutLine (TextLine line)
    {
        // How much to x-adjust all runs by to position correctly
        float xAdjust;

        // Which direction?
        if (_resolvedBaseDirection == TextDirection.LTR)
        {
            xAdjust = LayoutLineLTR (line);
        }
        else
        {
            /*
            if (_useMSWordStyleRtlLayout)
            {
                xAdjust = LayoutLineRTLWordStyle(line);
            }
            else
            */
            {
                xAdjust = LayoutLineRTL (line);
            }
        }

        // Process all runs, setting their final left-aligned x-position and
        // calculating metrics for the line as a whole
        float maxAbove = 0;
        float maxBelow = 0;
        float maxAscent = 0;
        float maxDescent = 0;
        for (var frIndex = 0; frIndex < line.Runs.Count; frIndex++)
        {
            // Get the run
            var fr = line.Runs[frIndex];

            // Adjust run position
            fr.XCoord += xAdjust;

            var above = -fr.Ascent + fr.HalfLeading;
            var below = fr.Descent + fr.HalfLeading;

            if (above > maxAbove)
            {
                maxAbove = above;
            }

            if (below > maxBelow)
            {
                maxBelow = below;
            }

            if (fr.Ascent < maxAscent)
            {
                maxAscent = fr.Ascent;
            }

            if (fr.Descent > maxDescent)
            {
                maxDescent = fr.Descent;
            }
        }

        // Store line metrics
        line.MaxAscent = maxAscent;
        line.MaxDescent = maxDescent;
        line.Height = maxAbove + maxBelow;
        line.BaseLine = maxAbove;
    }

    /// <summary>
    /// Layout a line (LTR edition)
    /// </summary>
    /// <param name="line">The line to be laid out</param>
    /// <returns>A final x-adjustment to be applied to the line's font runs</returns>
    private float LayoutLineLTR (TextLine line)
    {
        float x = 0;
        float trailingWhitespaceWidth = 0;
        for (var i = 0; i < line.Runs.Count; i++)
        {
            var fr = line.Runs[i];
            if (fr.Direction == TextDirection.LTR)
            {
                fr.XCoord = x;
                x += fr.Width;

                if (fr.RunKind == FontRunKind.TrailingWhitespace)
                {
                    trailingWhitespaceWidth += fr.Width;
                }
            }
            else
            {
                System.Diagnostics.Debug.Assert (fr.RunKind == FontRunKind.Normal);

                var j = i;
                while (j + 1 < line.Runs.Count && line.Runs[j + 1].Direction == TextDirection.RTL)
                    j++;

                var continueFrom = j;

                while (j >= i)
                {
                    fr = line.Runs[j];
                    fr.XCoord = x;
                    x += fr.Width;
                    j--;
                }

                i = continueFrom;
            }
        }

        // Store content width
        line.Width = x - trailingWhitespaceWidth;

        return 0;
    }

    /// <summary>
    /// Layout a line (RTL edition)
    /// </summary>
    /// <param name="line">The line to be laid out</param>
    /// <returns>A final x-adjustment to be applied to the line's font runs</returns>
    private float LayoutLineRTL (TextLine line)
    {
        float x = 0;
        float trailingWhitespaceWidth = 0;
        for (var i = 0; i < line.Runs.Count; i++)
        {
            var fr = line.Runs[i];
            if (fr.Direction == TextDirection.RTL)
            {
                x -= fr.Width;
                fr.XCoord = x;

                if (fr.RunKind == FontRunKind.TrailingWhitespace)
                {
                    trailingWhitespaceWidth += fr.Width;
                }
            }
            else
            {
                System.Diagnostics.Debug.Assert (fr.RunKind == FontRunKind.Normal);

                var j = i;
                while (j + 1 < line.Runs.Count && line.Runs[j + 1].Direction == TextDirection.LTR)
                    j++;

                var continueFrom = j;

                while (j >= i)
                {
                    fr = line.Runs[j];
                    x -= fr.Width;
                    fr.XCoord = x;
                    j--;
                }

                i = continueFrom;
            }
        }

        // Work out final width
        var totalWidth = -x - trailingWhitespaceWidth;

        // Store content width
        line.Width = totalWidth;

        // Everything is currently laid out from 0 to the left, so we
        // need to shift right to make it left aligned
        return totalWidth;
    }

    /// <summary>
    /// Layout a line (MS Word RTL edition)
    /// </summary>
    /// <param name="line">The line to be laid out</param>
    /// <returns>A final x-adjustment to be applied to the line's font runs</returns>
    private float LayoutLineRTLWordStyle (TextLine line)
    {
        float xPos = 0;
        float trailingWhitespaceWidth = 0;
        for (var i = 0; i < line.Runs.Count; i++)
        {
            // Get the direction of this group
            var groupDirection = line.Runs[i].Direction;

            // Count the number of runs with the same directionality
            var j = i;
            float groupWidth = 0;
            while (j < line.Runs.Count && line.Runs[j].Direction == groupDirection)
            {
                if (line.Runs[i].RunKind == FontRunKind.TrailingWhitespace)
                {
                    trailingWhitespaceWidth += line.Runs[i].Width;
                }

                groupWidth += line.Runs[j].Width;
                j++;
            }

            // How many runs in the group
            var runsInGroup = j - i;

            // Place the runs in the group
            if (groupDirection == TextDirection.LTR)
            {
                // Left to right group
                for (; i < j; i++)
                {
                    line.Runs[i].XCoord = xPos;
                    xPos += line.Runs[i].Width;
                }
            }
            else
            {
                // Right to left group
                xPos += groupWidth;
                var x = xPos;
                for (; i < j; i++)
                {
                    x -= line.Runs[i].Width;
                    line.Runs[i].XCoord = x;
                }
            }

            i = j - 1;
        }

        line.Width = xPos - trailingWhitespaceWidth;

        return _maxWidthResolved - (xPos - trailingWhitespaceWidth);
    }

    /// <summary>
    /// Finalize lines, positioning for horizontal alignment and
    /// moving all glyphs into position (relative to the text block top left)
    /// </summary>
    private void FinalizeLines()
    {
        // Work out the measured width
        foreach (var l in _lines)
        {
            if (l.Width > _measuredWidth)
            {
                _measuredWidth = l.Width;
            }
        }

        var ta = ResolveTextAlignment();
        foreach (var line in _lines)
        {
            // Work out x-alignement adjust for this line
            float xAdjust = 0;
            switch (ta)
            {
                case TextAlignment.Right:
                    xAdjust = (_maxWidth ?? _renderWidth ?? _measuredWidth) - line.Width;
                    break;

                case TextAlignment.Center:
                    xAdjust = ((_maxWidth ?? _renderWidth ?? _measuredWidth) - line.Width) / 2;
                    break;
            }

            // Position each run
            for (var frIndex = 0; frIndex < line.Runs.Count; frIndex++)
            {
                var fr = line.Runs[frIndex];
                fr.Line = line;
                fr.XCoord += xAdjust;
                fr.MoveGlyphs (fr.XCoord, line.YCoord + line.BaseLine);
            }
        }

        // Remove the font runs that weren't assigned to a line because the text
        // was truncated
        while (_fontRuns.Count > 0 && _fontRuns[_fontRuns.Count - 1].Line == null!)
        {
            _fontRuns.RemoveAt (_fontRuns.Count - 1);
        }
    }

    /// <summary>
    /// Finds the font run containing a specified code point index
    /// </summary>
    /// <param name="from">The font run index to start from</param>
    /// <param name="codePointIndex">The code point index being searched for</param>
    /// <returns>The index of the font run containing the specified code point
    /// index, or font run count if past the end</returns>
    private int FindFontRunForCodePointIndex (int from, int codePointIndex)
    {
        var frIndex = from;
        while (true)
        {
            var fr = _fontRuns[frIndex];
            if (codePointIndex < fr.Start)
            {
                System.Diagnostics.Debug.Assert (frIndex > 0);
                frIndex--;
            }
            else if (codePointIndex >= fr.End)
            {
                frIndex++;
                if (frIndex == _fontRuns.Count)
                {
                    System.Diagnostics.Debug.Assert (codePointIndex == _codePoints.Length);
                    return _codePoints.Length;
                }
            }
            else
            {
                return frIndex;
            }
        }
    }

    /// <summary>
    /// Re-usable buffer holding just the ellipsis character
    /// </summary>
    private static Utf32Buffer ellipsis = new ("…");

    /// <summary>
    /// Create a special font run containing the ellipsis character
    /// based on an existing run
    /// </summary>
    /// <param name="basedOn">The run to base the styling on</param>
    /// <returns>A new font run containing the ellipsis character</returns>
    private FontRun CreateEllipsisRun (FontRun basedOn)
    {
        // Get the type face
        var typeface = TypefaceFromStyle (basedOn.Style!, true);

        // Split into font fallback runs (there should only ever be just one)
        var fontRun = FontFallback.GetFontRuns (ellipsis.AsSlice(), typeface).Single();

        // Create the new run and mark is as a special run type for ellipsis
        var fr = CreateFontRun (basedOn.StyleRun!, ellipsis.SubSlice (fontRun.Start, fontRun.Length),
            _resolvedBaseDirection, basedOn.Style!, fontRun.Typeface, typeface);
        fr.RunKind = FontRunKind.Ellipsis;

        // Done
        return fr;
    }

    /// <summary>
    /// Update the current line with a new font run containing the trailing ellipsis
    /// </summary>
    /// <param name="line">The line to be updated</param>
    /// <param name="postLayout">True if the ellipsis is being added post layout via a user call to AddEllipsis()</param>
    private void AdornLineWithEllipsis (TextLine line, bool postLayout = false)
    {
        if (!_ellipsisEnabled)
        {
            return;
        }

        var lastRun = line.Runs[line.Runs.Count - 1];

        // Don't add ellipsis if the last run actually
        // has all the text...
        if (!postLayout && lastRun.End == _codePoints.Length)
        {
            return;
        }

        // Remove all trailing whitespace from the line
        for (var i = line.Runs.Count - 1; i >= 0; i--)
        {
            var r = line.Runs[i];
            if (r.RunKind == FontRunKind.TrailingWhitespace)
            {
                line.RunsInternal.RemoveAt (i);
                if (postLayout)
                {
                    _fontRuns.Remove (r);
                }
            }
        }

        // Calculate the total width of the line
        float totalWidth = 0;
        for (var i = 0; i < line.Runs.Count; i++)
        {
            totalWidth += line.Runs[i].Width;
        }

        // Get the new last run (if any)
        if (line.Runs.Count > 0)
        {
            lastRun = line.Runs[line.Runs.Count - 1];
        }

        // Create a new run for the ellipsis
        var ellipsisRun = CreateEllipsisRun (lastRun);

        // Work out how much room we've got.  If this is user request
        // to append the ellipsis and MaxWidth isn't set then make sure
        // we don't change our previously measured width be using that
        // previous measurement as the limit
        var maxWidth = _maxWidthResolved;

        var removeWidth = totalWidth + ellipsisRun.Width - maxWidth;
        if (removeWidth > 0)
        {
            for (var i = line.Runs.Count - 1; i >= 0; i--)
            {
                var fr = line.Runs[i];

                // Does this run have enough to remove?
                // No, remove it all
                if (fr.Width < removeWidth)
                {
                    removeWidth -= fr.Width;
                    line.RunsInternal.RemoveAt (i);
                    if (postLayout)
                    {
                        _fontRuns.Remove (fr);
                    }

                    continue;
                }

                // Work out where to split this run
                var pos = fr.FindBreakPosition (fr.Width - removeWidth, false);
                if (pos == fr.Start)
                {
                    // Nothing fits, remove it all
                    line.RunsInternal.RemoveAt (i);
                    if (postLayout)
                    {
                        _fontRuns.Remove (fr);
                    }
                }
                else
                {
                    // Split it
                    var remaining = fr.Split (pos);

                    // Keep the remaining part in case we need it later (not sure why,
                    // but seems wise).
                    if (!postLayout)
                    {
                        _fontRuns.Insert (_fontRuns.IndexOf (fr) + 1, remaining);

//                            _fontRuns.Remove(fr);
                    }
                }

                break;
            }
        }

        // Add it to the line
        line.RunsInternal.Add (ellipsisRun);

        if (postLayout)
        {
            // No layout adjustmenst if this is post layout change
            LayoutLine (line);
        }
        else
        {
            // Remember old line height
            var oldHeight = line.Height;
            _measuredHeight -= line.Height;

            // Layout the line again
            LayoutLine (line);

            // Adjust height just in case it changed
            _measuredHeight += line.Height;
        }
    }

    /// <summary>
    /// Check if the current layout has exceeded any height restrictions
    /// and if so, remove any offending lines and optionally create the
    /// ellipsis at the end indicating the text has been truncated
    /// </summary>
    /// <returns>True if can continue adding lines; otherwise false</returns>
    private bool CheckHeightConstraints()
    {
        // Special case for the first line not fitting
        if (_lines.Count == 1 && _measuredHeight > _maxHeightResolved)
        {
            _truncated = true;
            _measuredWidth = 0;
            _lines.RemoveAt (0);
            return false;
        }

        // Have we exceeded the height limit
        if (_measuredHeight > _maxHeightResolved || _maxHeightResolved <= 0)
        {
            // Remove the last line (unless it's the only line)
            if (_lines.Count > 1)
            {
                _measuredHeight -= _lines[_lines.Count - 1].Height;
                _lines.RemoveAt (_lines.Count - 1);
            }

            if (_lines.Count > 0)
            {
                AdornLineWithEllipsis (_lines[_lines.Count - 1]);
            }

            _truncated = true;
            return false;
        }

        // Have we hit the line count limit?
        if (_lines.Count >= _maxLinesResolved)
        {
            if (_lines.Count > 0)
            {
                AdornLineWithEllipsis (_lines[_lines.Count - 1]);
            }

            _truncated = true;
            return false;
        }

        return true;
    }

    /// <summary>
    /// Resets and internal object and memory pools.
    /// </summary>
    /// <remarks>
    /// For performance reasons and to reduce pressure on the
    /// garbage collector, RichTextKit maintains several internal
    /// per-thread memory and object pools.
    ///
    /// If you create a very large text block, these pools will be
    /// enlarged to cope with content of larger text blocks.
    ///
    /// This method can be used to reset those pools to reclaim the
    /// extra memory they consumed.
    ///
    /// In general you can ignore this method, unless you know you're
    /// working with very large text blocks (which you shouldn't be
    /// anyway, since a text block is only supposed to be a single
    /// paragraph).
    /// /// </remarks>
    public static void ResetPooledMemory()
    {
        TextLine.Pool.Value = new ObjectPool<TextLine>();
        FontRun.Pool.Value = new ObjectPool<FontRun>();
        StyleRun.Pool.Value = new ObjectPool<StyleRun>();
        Bidi.Instance.Value = new Bidi();
    }
}
